// Copyright 2009 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: Jeff Stewart
//
// Reads and validates a Thunderbird mail configuration.
//
// Thunderbird configuration:
// In Documents and Settings\Application data (found via getenv(APPDATA))
// find a directory called Thunderbird.
// In there is an INI file, profiles.ini.
// That file has a section for each profile providing a name and path.
// That path is a directory.  In that directory, find prefs.js, containing
// machine generated javascript.  That file contains a series of
// lines that look like this:
// user_pref("app.update.lastAppVersion", "1.5.0.4");
//
// The values we're interested in are the following,
// where value may be an integer, a boolean, a quoted string:
// mail.smtpservers: comma separated list of server names.
//                   Assume SMTP1 in below:
// mail.smtpserver.SMTP1.username: string value, email address
// mail.smtpserver.SMTP1.realusername: string value, if present,
//                                                   preempts username
// mail.smtpserver.SMTP1.port: integer: port number
// mail.smtpserver.SMTP1.try_ssl: integer:  3 appears to be SSL
// mail.smtpserver.SMTP1.hostname: string hostname
// mail.smtpserver.SMTP1.realhostname: string, if present preempts hostname
// mail.smtpserver.SMTP1.description: don't care
// mail.smtpserver.SMTP1.auth_method: integer, is 1 for me
// mail.smtp.defaultserver: string, gives the name of default outbound server
// mail.accountmanager.accounts: string, comma separated account name list,
//                               assume ACCOUNT1 below
// mail.accountmanager.defaultaccount: string, single entry default account
// mail.account.ACCOUNT1.server: label of the server.  Assume SERVER1 below
// mail.account.ACCOUNT1.identities: (optional): string: valud of ID for this
//                                               server.  Assume ID1 below.
// mail.identity.ID1.smtpServer: label of entry under mail.smtpserver.
//                               Assume SMTP1 above.
// mail.identity.ID1.useremail: string, email address of user
// mail.identity.ID1.fullName: string, display name
// mail.server.SERVER1.type: string: we want "pop3" or "imap".  "none"
//                           seems to be local folders
// mail.server.SERVER1.hostname: string, "pop.ourdomain", "imap.ourdomain"
// mail.server.SERVER1.realhostname: string, if present preempts hostname
// mail.server.SERVER1.name: probably not important
// mail.server.SERVER1.port: numeric, port number
// mail.server.SERVER1.userName: string: email address to login with
// mail.server.SERVER1.realhostname: string, if present preempts username

#include <QtCore/QString>
#include <QtCore/QChar>
#include <QtCore/QDir>
#include <QtCore/QMap>
#include <QtCore/QSet>
#include <QtCore/QFile>
#include <QtCore/QRegExp>

#ifdef _WIN32
#include <windows.h>  // for WCHAR
#endif
#include <cstdlib>

#include "config/thunderbird.h"

#include "util/common.h"
#include "util/protocol.h"
#include "util/provider.h"

namespace popdiag {

class IniFileReader {
 public:
  // NOTE: this class will NOT delete readme on destruction
  explicit IniFileReader(QIODevice *readme);
  ~IniFileReader();
  void GetSectionNames(QSet<QString> *out);
  bool GetSectionMap(const QString &section, QMap<QString, QString> *out);
  bool GetEntry(const QString &section, const QString &name, QString *value);
  bool Read(QTextStream *log);
 private:
  QIODevice *io_;
  typedef QMap<QString, QString> StrStrMap;
  typedef QMap<QString, StrStrMap> PrefStorage;

  PrefStorage prefs_;
};

class PrefsJsReader {
 public:
  // NOTE: this class will NOT delete readme on destruction
  explicit PrefsJsReader(QIODevice *readme);
  ~PrefsJsReader();
  bool Read(QTextStream *log);

  static PrefsJsReader *Load(const QString &path, QTextStream *log);

  QString ReadEntry(const QString &name) const;
 private:
  QIODevice *io_;
  typedef QMap<QString, QString> Storage;
  Storage map_;
};

IniFileReader::IniFileReader(QIODevice *src) : io_(src) {
}

IniFileReader::~IniFileReader() {
}

bool IniFileReader::Read(QTextStream *log) {
  *log << "Reading profiles.ini...\n";
  QString section_name;
  while (true) {
    char buffer[1024];
    qint64 read_bytes = io_->readLine(buffer, sizeof(buffer));
    if (read_bytes <= 0) {
      return true;
    }
    QString line = QString::fromUtf8(buffer, read_bytes).trimmed();
    QRegExp section_pattern("^\\s*\\[([^\\]]*)\\]\\s*$");
    QRegExp entry_pattern("^([^=]+)+=(.*)$");
    if (0 == section_pattern.indexIn(line)) {
      section_name = section_pattern.cap(1);
      *log << "New section: " << section_name << "\n";
    } else {
      if (0 == entry_pattern.indexIn(line)) {
        QString name = entry_pattern.cap(1);
        QString value = entry_pattern.cap(2);
        prefs_[section_name][name] = value;
        *log << name << " = " << value << "\n";
      }
    }
  }
}

void IniFileReader::GetSectionNames(QSet<QString> *out) {
  out->clear();
  for (PrefStorage::const_iterator i = prefs_.begin();
       i != prefs_.end(); ++i) {
    out->insert(i.key());
  }
}

bool IniFileReader::GetSectionMap(const QString &section,
    QMap<QString, QString> *out) {
  PrefStorage::const_iterator siter = prefs_.find(section);
  if (siter == prefs_.end()) {
    return false;
  }
  out->clear();
  *out = siter.value();
  return true;
}

bool IniFileReader::GetEntry(const QString &section, const QString &name,
    QString *value) {
  PrefStorage::const_iterator siter = prefs_.find(section);
  if (siter == prefs_.end()) {
    return false;
  }
  StrStrMap::const_iterator kiter = siter.value().find(name);
  if (kiter == siter.value().end()) {
    return false;
  }
  *value = kiter.value();
  return true;
}


// PrefsJsReader can read in Thunderbird prefs.js and provide
// structured access to the data we parse from it.  It's very
// primitive and may need to be updated once we get a broader
// set of prefs.js files to inspect.
PrefsJsReader::PrefsJsReader(QIODevice *src) : io_(src) {
}

PrefsJsReader::~PrefsJsReader() {
}

// Read in the given prefs.js file and provide and object to
// read the results in a structured manner.
PrefsJsReader *PrefsJsReader::Load(const QString &prefs_js_path,
                                   QTextStream *log) {
  QFile prefsjs(prefs_js_path);
  if (!prefsjs.open(QIODevice::ReadOnly)) {
    *log << "Cannot read file: [" << prefs_js_path << "]\n";
    return NULL;
  }

  scoped_ptr<PrefsJsReader> pjr(new PrefsJsReader(&prefsjs));
  if (!pjr->Read(log)) {
    return NULL;
  }

  return pjr.release();
}

bool PrefsJsReader::Read(QTextStream *log) {
  *log << " reading prefs.js...\n";
  while (true) {
    char buffer[1024];
    qint64 read_bytes = io_->readLine(buffer, sizeof(buffer));
    if (read_bytes <= 0) {
      return true;
    }
    QString line = QString::fromUtf8(buffer, read_bytes).trimmed();

    // not a very smart reader.  Can read the version written out by
    // thunderbird.  Cannot read all javascript.
    // read lines like this:
    // user_pref("app.update.lastAppVersion", "1.5.0.4");
    QRegExp pattern("^user_pref\\(\"([^\"]+)\",\\s*([^)]+)\\);\\s*$");
    if (0 == pattern.indexIn(line)) {
      const QString &name = pattern.cap(1).trimmed();
      QString value = pattern.cap(2).trimmed();
      if (value.startsWith("\"") && value.endsWith("\"")) {
        value = value.section("\"", 1, 1);
      }
      map_[name] = value;
      *log << name << " = " << value << "\n";
    }
  }
}

QString PrefsJsReader::ReadEntry(const QString &name) const {
  Storage::const_iterator i = map_.find(name);
  if (i == map_.end()) {
    return QString("");
  }
  return i.value();
}

Thunderbird::Thunderbird() {
}

Thunderbird::~Thunderbird() {
}

QString Thunderbird::program_name() const {
  return tr("Mozilla Thunderbird",
            "The email reader program name");
}

#if defined (_WIN32)
namespace {

QString QStringFromWcharArray(const WCHAR *in) {
  QString out;
  while (*in) {
    out.append(QChar(*in));
    in++;
  }
  return out;
}

}  // anonymous namespace

// Find the data directory for Thunderbird.
// This will have to be updated on each platform.
QStringList Thunderbird::GetDataDirectoryLocations() const {
  // "my application data"
  wchar_t *dirpath = NULL;
  if (NULL == (dirpath = _wgetenv(L"APPDATA"))) {
    return NULL;
  }
  return QStringList(QStringFromWcharArray(dirpath) + "/Thunderbird");
}

#elif defined (__APPLE__)

QStringList Thunderbird::GetDataDirectoryLocations() const {
  // on Mac, thunderbird config is stored under the home directory, in
  // /Library/Thunderbird:
  QDir home = QDir::home();
  return QStringList(home.filePath("Library/Thunderbird"));
}

#else   // __unix

QStringList Thunderbird::GetDataDirectoryLocations() const {
  QDir home = QDir::home();
  // Thunderbird version-dependent directory locations in descending order,
  // so that we prefer the most recent one in the evaluation below.
  return (QStringList() <<
          // Thunderbird 3.0.3
          home.filePath(".thunderbird") <<
          // Thunderbird 2.0.0.16
          home.filePath(".mozilla-thunderbird"));
}

#endif  // __unix

bool Thunderbird::GetDataDirectory(QDir *dir_out) const {
  QStringList path_list = Thunderbird::GetDataDirectoryLocations();
  for (int i = 0; i < path_list.size(); ++i) {
    const QString &expected_path = path_list.at(i);
    if (expected_path != NULL) {
      QDir expected_dir(expected_path);
      if (expected_dir.exists() && expected_dir.isReadable()) {
        *dir_out = expected_dir;
        return true;
      }
    }
  }
  return false;
}

// Read in profiles.ini and provide a structured object to access
// the data in it.
IniFileReader *Thunderbird::ReadProfilesIni(QDir *profile_base_dir) const {
  QDir data_dir;
  if (!GetDataDirectory(&data_dir)) {
    return NULL;
  }
  *profile_base_dir = data_dir;
  QString profile_path = data_dir.filePath("profiles.ini");
  QFile profile(profile_path);
  if (!profile.open(QIODevice::ReadOnly)) {
    return NULL;
  }

  scoped_ptr<IniFileReader> mpr(new IniFileReader(&profile));
  QString str;
  QTextStream log(&str, QIODevice::WriteOnly);
  if (!mpr->Read(&log)) {
    // TODO: Find a way to relay these faults.
    return NULL;
  }

  return mpr.release();
}

bool Thunderbird::CheckConfiguration(VerificationSet *out) const {
  QDir profile_base_dir;
  scoped_ptr<IniFileReader> mpr(ReadProfilesIni(&profile_base_dir));
  if (mpr == NULL) {
    return false;
  }
  QSet<QString> sections;
  mpr->GetSectionNames(&sections);
  sections.remove("General");
  for (QSet<QString>::const_iterator i = sections.begin();
       i != sections.end(); ++i) {
    const QString &section = *i;
    QString path;
    QString is_relative;
    QString name;
    if (mpr->GetEntry(section, "Name", &name)
        && mpr->GetEntry(section, "Path", &path)
        && mpr->GetEntry(section, "IsRelative", &is_relative)) {
      QString dir_path;
      if (is_relative.toInt() != 0) {
        dir_path = profile_base_dir.filePath(path);
      } else {
        dir_path = path;
      }

      QDir dir(dir_path);
      if (!dir.exists() || !dir.isReadable()) {
        continue;
      }
      QString prefs_js_path = dir.filePath("prefs.js");
      if (!QFile::exists(prefs_js_path)) {
        continue;
      }
      ReadProfile(name, prefs_js_path, out);
    }
  }
  return false;
}

// We found a profile. Remember, we're going to present these to the
// user to verify. So they'll be able to pick the right profile from
// a list of possibilities. We might want to restrict this to only
// those profiles that use our email addresses or appropriate servers.
// However, this might result in misconfigured profiles not being shown.
// Thus, present all profiles.
void Thunderbird::ReadProfile(const QString &profile_name,
                              const QString &prefs_js_path,
                              VerificationSet *results) const {
  QString debug_log;
  QTextStream log(&debug_log, QIODevice::WriteOnly);
  scoped_ptr<PrefsJsReader> prefsjs(PrefsJsReader::Load(prefs_js_path, &log));
  if (prefsjs == NULL) {
    return;
  }
  // enumerate the accounts.  Look only at pop3 and imap type accounts
  // find accounts whose pop/imap server is ours or whose email address
  // matches.
  QStringList accounts =
      prefsjs->ReadEntry("mail.accountmanager.accounts").split(",");
  for (QStringList::const_iterator i = accounts.begin();
       i != accounts.end(); ++i) {
    const QString account_prefix = "mail.account." + *i + ".";
    QString server_label = prefsjs->ReadEntry(account_prefix + "server");

    const QString server_prefix = "mail.server." + server_label + ".";
    QString server_type = prefsjs->ReadEntry(server_prefix + "type");
    if (server_type != "pop3" && server_type != "imap") {
      continue;
    }

    // treat all profiles as interesting.
    ThunderbirdInspection *result = new ThunderbirdInspection(
      profile_name, prefs_js_path, server_prefix, account_prefix,
      server_type == "pop3" ? ThunderbirdInspection::SERVERTYPE_POP :
                              ThunderbirdInspection::SERVERTYPE_IMAP);
    result->Recalculate("");
    // must be recalculated once the email address is known
    results->append(result);
  }
}

ThunderbirdInspection::ThunderbirdInspection(const QString &profile_name,
                                             const QString &prefs_js_path,
                                             const QString &server_prefix,
                                             const QString &account_prefix,
                                             enum ServerType server_type)
  : profile_name_(profile_name),
    prefs_js_path_(prefs_js_path), server_prefix_(server_prefix),
    account_prefix_(account_prefix) {
  server_type_ = server_type;
}

void ThunderbirdInspection::Recalculate(const QString &email) {
  email_ = email.toLower();
  ClearLog();
  StartLogDetails();
  faults_.clear();

  QString debug_string;
  QTextStream log(&debug_string, QIODevice::WriteOnly);
  scoped_ptr<PrefsJsReader> prefsjs(PrefsJsReader::Load(prefs_js_path_, &log));
  if (prefsjs == NULL) {
    LogDetails("Parse prefs.js", false, debug_string);
    faults_.push_back(Fault(PROFILE_NOT_UNDERSTOOD, ""));
  } else {
    QString last_version = prefsjs->ReadEntry("extensions.lastAppVersion");
    QString name = "Mozilla Thunderbird " + last_version;  // not translated
    LogDetails("Program", true, name);
    set_program_name(tr("Mozilla Thunderbird") + " " + last_version);
    set_program_code("mt");  // do NOT translate this.
    QString profile_name = profile_name_ + ": " +
                           prefsjs->ReadEntry(server_prefix_ + "name");
    set_profile_name(profile_name);
    LogDetails("Profile", true, profile_name);
    if (ConfigVerifierRegistry::GetDefaultMailProgramBaseName()
        .indexOf("thund") == 0) {
      set_is_default_program(true);
    }

    CheckProfile(*prefsjs.get());
  }
  EndLogDetails();
}

// We found a probably interesting profile in this prefs file at
// server_prefix.  Enumerate all the faults with is and add them
// to the faultset
void ThunderbirdInspection::CheckProfile(const PrefsJsReader &prefsjs) {
  InitProtocolDependentValues();
  // See this page for some details:
  // http://www.mozilla.org/mailnews/arch/AccountRenaming.txt
  // If "realhostname" and "realusername" are present, prefer them.  Otherwise,
  // fall back to hostname and username.
  incoming_server_name_ = prefsjs.ReadEntry(server_prefix_ +
                                                   "realhostname");
  if (incoming_server_name_.isEmpty()) {
    incoming_server_name_ = prefsjs.ReadEntry(server_prefix_ + "hostname");
  }
  incoming_user_name_ = prefsjs.ReadEntry(server_prefix_ +
                                                 "realuserName");
  if (incoming_user_name_.isEmpty()) {
    incoming_user_name_ = prefsjs.ReadEntry(server_prefix_ + "userName");
  }

  incoming_socket_type_ = static_cast<SocketType>(
      prefsjs.ReadEntry(server_prefix_ + "socketType").toInt());

  QString port_string = prefsjs.ReadEntry(server_prefix_ + "port");
  incoming_port_ = port_string.toInt();
  if (incoming_port_ == 0) {
    switch (incoming_socket_type_) {
      case SSL:
        incoming_port_ = ssl_port_;
        break;
      case TLS_IF_AVAILABLE:  // fall through
      case TLS:               // fall through
      case PLAINTEXT:         // fall through
      default:
        incoming_port_ = plaintext_port_;
    }
  }

  // server timeout is not user configurable on Thunderbird, so POP3_TIMEOUT
  // not applicable

  if (server_type_ == SERVERTYPE_POP) {
    QString leave_on_server_string = prefsjs.ReadEntry(
      server_prefix_ + "leave_on_server");
    leave_on_server_ = leave_on_server_string == "true";
  }

  CheckIncomingSettings();

  // SMTP settings now:
  QStringList ids = prefsjs.ReadEntry(account_prefix_ + "identities")
                    .split(",");
  if (ids.isEmpty()) {
    ids.append("id1");
  }

  bool found_our_smtp = false;

  for (QStringList::iterator i = ids.begin(); i != ids.end(); ++i) {
    QString id = *i;
    QString id_prefix = "mail.identity." + id + ".";

    QString smtp_server = prefsjs.ReadEntry(id_prefix + "smtpServer");
    const QString smtp_prefix = "mail.smtpserver." + smtp_server + ".";
    QString smtp_host = prefsjs.ReadEntry(smtp_prefix + "realhostname")
                        .toLower();
    if (smtp_host.isEmpty()) {
      smtp_host = prefsjs.ReadEntry(smtp_prefix + "hostname").toLower();
    }
    QString full_name = prefsjs.ReadEntry(id_prefix + "fullName");
    set_display_name(full_name);
    smtp_email_ = prefsjs.ReadEntry(id_prefix + "useremail").toLower();
    set_display_email(smtp_email_);
    smtp_server_ = smtp_host;
    smtp_username_ = prefsjs.ReadEntry(smtp_prefix + "realusername");
    if (smtp_username_.isEmpty()) {
      smtp_username_ = prefsjs.ReadEntry(smtp_prefix + "username");
    }
    int auth_method = prefsjs.ReadEntry(smtp_prefix + "auth_method").toInt();
    smtp_use_auth_ = auth_method == 1;

    smtp_socket_type_ = static_cast<SocketType>(
        prefsjs.ReadEntry(smtp_prefix + "try_ssl").toInt());

    QString port_string = prefsjs.ReadEntry(smtp_prefix + "port");
    smtp_port_ = port_string.toInt();
    if (port_string.isEmpty()) {
      switch (smtp_socket_type_) {
        case SSL:
          smtp_port_ = SMTP_MSA_SSL_PORT;
        default:  // all other cases:
          smtp_port_ = SMTP_MX_PLAINTEXT_PORT;
      }
    }
    if (Protocol::SMTP().IsOurMailServer(smtp_host)) {
      found_our_smtp = true;
      break;
    }
  }
  if (!found_our_smtp) {
    LogDetails("SMTP Server", false, "No matching smtp server found");
    faults_.append(Fault(SMTP_SERVER, ""));
  } else {
    CheckOutgoingSettings();
  }
}

}  // namespace popdiag
